﻿/*
 *  This file is part of SXAPI.
 *
 *  SXAPI is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, version 3 of the License.

 *  SXAPI is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *  
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with SXAPI.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.Globalization;

namespace StackExchangeApi
{
    /// <summary>
    /// Base class for all SearchConstraints.
    /// </summary>
    public abstract class SearchConstraint
    {
        /// <summary>
        /// Determine whether two SearchConstraints are "compatible".
        /// 
        /// Incompatible constraints are guaranteed to return 0 results.
        /// </summary>
        /// <param name="other">The other SearchConstraint</param>
        /// <returns>True if compatible, false otherwise</returns>
        internal virtual bool CompatibleWith(SearchConstraint other) { return true; }

        /// <summary>
        /// Returns the string that, when appended to other fragments of a search string,
        /// will constrain the results of a search as expected.
        /// </summary>
        /// <returns>As above</returns>
        internal abstract String SearchString();
    }

    /// <summary>
    /// A non-user constructable custom search constraint.
    /// 
    /// Does not support usage with any other constraints.
    /// </summary>
    internal sealed class CustomSearchConstraint : SearchConstraint
    {
        /// <summary>
        /// The search term used to construct this CustomSearchConstraint
        /// </summary>
        internal String Term { get; set; }

        /// <summary>
        /// Construct a new CustomSearchConstraint
        /// </summary>
        /// <param name="term">The search term</param>
        public CustomSearchConstraint(String term)
        {
            Term = term;
        }

        internal override bool CompatibleWith(SearchConstraint other)
        {
            return other == this;
        }

        internal override string SearchString()
        {
            return Term;
        }
    }

    /// <summary>
    /// Search for Posts that contain a
    /// certain phrase.
    /// </summary>
    public sealed class ContainsPhrase : SearchConstraint
    {
        /// <summary>
        /// The phrase to search for
        /// </summary>
        public String Phrase { get; internal set; }

        /// <summary>
        /// Construct a new ContainsPhrase SearchConstraint.
        /// </summary>
        /// <param name="phrase">The phrase to search for</param>
        public ContainsPhrase(String phrase)
        {
            Phrase = phrase;
        }

        internal override string SearchString()
        {
            return
                String.Format(
                  CultureInfo.InvariantCulture,
                  "\"{0}\"",
                  Phrase);
        }
    }

    /// <summary>
    /// Search for Posts that contain
    /// at least one of the provided words.
    /// </summary>
    public sealed class ContainsWords : SearchConstraint
    {
        /// <summary>
        /// The words in question
        /// </summary>
        public ReadOnlyCollection<String> Words { get; internal set; }

        /// <summary>
        /// Construct a new ContainsWords
        /// </summary>
        /// <param name="words">The words to search for, cannot contain whitespace</param>
        public ContainsWords(params String[] words)
        {
            foreach (String word in words)
                foreach (char c in word)
                    if (Char.IsWhiteSpace(c))
                        throw new ArgumentException(
                            String.Format(
                              CultureInfo.InvariantCulture,
                              "Words in `words` cannot contain whitespace; violating word [{0}]",
                              word));

            Words = new List<String>(words).AsReadOnly();
        }

        internal override bool CompatibleWith(SearchConstraint other)
        {
            if (other is WordsInTitle) return false;

            return base.CompatibleWith(other);
        }

        internal override string SearchString()
        {
            return String.Join(" ", Words.ToArray());
        }
    }

    /// <summary>
    /// A constraint that the given post either has the given tag,
    /// or is an Answer to a Question with the given tag.
    /// </summary>
    public sealed class HasTag : SearchConstraint
    {
        /// <summary>
        /// The tag in question
        /// </summary>
        public String Tag { get; internal set; }

        /// <summary>
        /// Construct a new HasTag
        /// </summary>
        /// <param name="tag">The tag in question, must be lowercase and cannot contain whitespace</param>
        public HasTag(String tag)
        {
            if (tag.ToLowerInvariant() != tag)
                throw new ArgumentException("tag must be lower case");

            foreach (char c in tag)
                if (Char.IsWhiteSpace(c))
                    throw new ArgumentException("tag cannot contain white space");

            Tag = tag;
        }

        internal override string SearchString()
        {
            return
                String.Format(
                  CultureInfo.InvariantCulture,
                  "[{0}]",
                  Tag);
        }
    }

    /// <summary>
    /// Search for Posts by a specific User
    /// </summary>
    public sealed class ByUser : SearchConstraint
    {
        /// <summary>
        /// The User in question
        /// </summary>
        public User User { get; internal set; }

        /// <summary>
        /// Construct a new ByUser constraint.
        /// </summary>
        /// <param name="user">The user in question</param>
        public ByUser(User user) { User = user; }

        internal override string SearchString()
        {
            return
                String.Format(
                  CultureInfo.InvariantCulture,
                  "user:{0}",
                  User.Id);
        }
    }

    /// <summary>
    /// Search for posts with a minimum number of votes.
    /// </summary>
    public sealed class MinimumVotes : SearchConstraint
    {
        /// <summary>
        /// Vote minimum
        /// </summary>
        public long MinVotes { get; internal set; }

        /// <summary>
        /// Construct a new MinimumVotes
        /// </summary>
        /// <param name="min">The vote cutoff</param>
        public MinimumVotes(long min) { MinVotes = min; }

        internal override string SearchString()
        {
            return
                String.Format(
                  CultureInfo.InvariantCulture,
                  "votes:{0}",
                  MinVotes);
        }
    }

    /// <summary>
    /// Only search for posts which are "Community Wiki"
    /// </summary>
    public sealed class IsCommunityWiki : SearchConstraint
    {
        /// <summary>
        /// Singleton instance of this SearchConstraint
        /// </summary>
        public static readonly IsCommunityWiki Singleton = new IsCommunityWiki();

        private IsCommunityWiki() { }

        internal override string SearchString()
        {
            return "wiki:1";
        }
    }

    /// <summary>
    /// Abstract class from which all constraints that
    /// result in ONLY Questions being returned descend.
    /// 
    /// It is by definition an error for a QuestionSearchConstraint
    /// to be on the same SearchQuery as an AnswerSearchConstraint
    /// </summary>
    public abstract class QuestionSearchConstraint : SearchConstraint
    {
        internal override bool CompatibleWith(SearchConstraint other)
        {
            if (other is AnswerSearchConstraint) return false;

            return true;
        }
    }

    /// <summary>
    /// Search for questions with certain words in the title
    /// </summary>
    public sealed class WordsInTitle : QuestionSearchConstraint
    {
        /// <summary>
        /// Words that must appear in the Question's Title.
        /// </summary>
        public ReadOnlyCollection<String> Words { get; internal set; }

        /// <summary>
        /// Construct a new WordsInTitle.
        /// </summary>
        /// <param name="words">The words to search for</param>
        public WordsInTitle(params String[] words)
        {
            Words = new List<String>(words).AsReadOnly();
        }

        internal override string SearchString()
        {
            return
                String.Format(
                  CultureInfo.InvariantCulture,
                  "{0} intitle:1",
                  String.Join(" ", Words.ToArray()));
        }

        internal override bool CompatibleWith(SearchConstraint other)
        {
            if (other is ContainsPhrase) return false;
            if (other is ContainsWords) return false;

            return base.CompatibleWith(other);
        }
    }

    /// <summary>
    /// Search for Questions with a minimum number of views
    /// </summary>
    public sealed class MinimumViews : QuestionSearchConstraint
    {
        /// <summary>
        /// The Cutoff number of views
        /// </summary>
        public long MinViews { get; internal set; }

        /// <summary>
        /// Construct a new MinimumViews constraint.
        /// </summary>
        /// <param name="min">The cutoff number of views</param>
        public MinimumViews(long min) { MinViews = min; }

        internal override string SearchString()
        {
            return
                String.Format(
                  CultureInfo.InvariantCulture,
                  "views:{0}",
                  MinViews);
        }
    }

    /// <summary>
    /// Search for Questions that have answers.
    /// </summary>
    public sealed class HasAnswer : QuestionSearchConstraint
    {
        /// <summary>
        /// Singleton instance of HasAnswer
        /// </summary>
        public static readonly HasAnswer Singleton = new HasAnswer();

        private HasAnswer() { }

        internal override string SearchString()
        {
            return "hasaccepted:1";
        }
    }

    /// <summary>
    /// Search for Questions that have a minimum number of answers
    /// </summary>
    public sealed class MinimumAnswerCount : QuestionSearchConstraint
    {
        /// <summary>
        /// The cutoff number of answers
        /// </summary>
        public long MinAnswers { get; internal set; }

        /// <summary>
        /// Construct a new MinimumAnswerCount constraint.
        /// </summary>
        /// <param name="count">the cutoff number of answers</param>
        public MinimumAnswerCount(long count) { MinAnswers = count; }

        internal override string SearchString()
        {
            return
                String.Format(
                  CultureInfo.InvariantCulture,
                  "answers:{0}",
                  MinAnswers);
        }
    }

    /// <summary>
    /// Search only for closed Questions.
    /// </summary>
    public sealed class IsClosed : QuestionSearchConstraint
    {
        /// <summary>
        /// Singleton instance of isClosed
        /// </summary>
        public static readonly IsClosed Singleton = new IsClosed();

        private IsClosed() { }

        internal override string SearchString()
        {
            return "closed:1";
        }
    }

    /// <summary>
    /// Abstract class from which all constraints that
    /// result in ONLY Answers being returned descend.
    /// 
    /// It is by definition an error for a AnswerSearchConstraint
    /// to be on the same SearchQuery as an QuestionSearchConstraint.
    /// </summary>
    public abstract class AnswerSearchConstraint : SearchConstraint {
        
        internal override bool CompatibleWith(SearchConstraint other)
        {
            if (other is QuestionSearchConstraint) return false;

            return true;
        }
    }

    /// <summary>
    /// Only search for Answers which have been accepted.
    /// </summary>
    public sealed class IsAcceptedAnswer : AnswerSearchConstraint
    {
        /// <summary>
        /// Singleton instance of IsAcceptedAnswer
        /// </summary>
        public static readonly IsAcceptedAnswer Singleton = new IsAcceptedAnswer();

        private IsAcceptedAnswer() { }

        internal override string SearchString()
        {
            return "isaccepted:1";
        }
    }
}
